package block

import (
	"bytes"
	"crypto/ecdsa"
	"errors"
	"fmt"
	"github.com/boltdb/bolt"
	"publicChainSystem/transaction"
	"publicChainSystem/wallet"
)

/**
 *@author:zhengyilong
 *@email:1797344574@qq.com
 *@phone:13479394730
 *@DateTime:2022/2/21 20:21
 **/

const CHAIN_DB_PATH = "./chain.db"	//要保存的文件地址
const BUCKET_BLOCK = "chain_blocks"	//存区块的桶的名字
const BUCKET_STATUS = "chain_status"//存最后一个区块的hash值
const LAST_HASH = "last_hash"		//桶2的key的名字

//区块链的作用 用来管理区块的
type BlockChain struct {
	//Blocks []*Block
	DB *bolt.DB
	LastHash []byte
	Wallet *wallet.Wallet
}

//创建区块链，如果区块链已存在，则不会创建，只是返回一个对象。
func NewChain(address string) (*BlockChain,error){
	//bc.Blocks = []*Block{}	//实例化区块链，赋值一个空的切片，用来存储新的区块

	db, err := bolt.Open(CHAIN_DB_PATH, 0600, nil)
	//defer db.Close()
	if err != nil {
		return nil,err
	}

	isFirst := false //表示是否是第一次执行，如果是true那么代表是第一次执行
	var private *ecdsa.PrivateKey

	//实例化一个wallet，实现创建区块链的同时创建钱包
	wlt, err := wallet.NewWallet(db)
	if err != nil {
		return nil,err
	}

	var lastHash []byte
	//update方法，同一时间内，只能有一个人来查看数据
	err = db.Update(func(tx *bolt.Tx) error {

		//使用桶，若不存在则创建
		bk := tx.Bucket([]byte(BUCKET_BLOCK))
		if bk==nil {
			//创建区块链的时候由系统给地址，此处创建一个地址
			addr,pri, err := wlt.NewAddress()
			fmt.Println("创世区块的奖励地址为",addr)
			if err != nil {
				return err
			}
			private = pri
			address = addr
			isFirst = true

			//创建coinbase交易
			coinbase, err := transaction.NewCoinbase(address)
			if err != nil {
				return err
			}

			bk, err = tx.CreateBucket([]byte(BUCKET_BLOCK))
			if err != nil {
				return err
			}

			//创建一个创世区块
			genesis := GenesisBlock(*coinbase)
			//将区块进行序列化
			serialize, err := genesis.Serialize()
			if err != nil {
				return err
			}

			//将创世区块添加到桶里面
			bk.Put(genesis.Hash,serialize)

			//创建桶2
			bk2, err := tx.CreateBucket([]byte(BUCKET_STATUS))
				if err != nil {
					return err
			}

			//把最后一个区块的哈希值放到另一个桶中
			bk2.Put([]byte(LAST_HASH),genesis.Hash)

			lastHash = genesis.Hash

		}else {
			//桶已存在，打开桶2将最后一个区块哈希值赋值给结构体中
			bk2 := tx.Bucket([]byte(BUCKET_STATUS))
			lastHash = bk2.Get([]byte(LAST_HASH))
		}

		return nil
	})

	//如果是第一次创建，则将地址和私钥保存进桶中
	if isFirst {
		err := wlt.SavePrivateKey(address, private)
		if err != nil {
			return nil,err
		}
	}

	bc := BlockChain{
		DB: db,
		LastHash: lastHash,
		Wallet: wlt,
	}

	//把创建好的创世区块存到区块链中
	//bc.Blocks = append(bc.Blocks,genesis)

	return &bc,err
}

//把区块添加到区块中
func (bc *BlockChain) AddBlock(tx []transaction.Transaction) error{
	//1、创建区块
	newBlock := NewBlock(bc.LastHash,tx)

	//update方法，同一时间内，只能有一个人来查看数据
	err := bc.DB.Update(func(tx *bolt.Tx) error {
		//使用桶
		bk := tx.Bucket([]byte(BUCKET_BLOCK))
		if bk == nil {
			return errors.New("桶中没有区块")
		}

		//将新区块进行序列化
		serialize, err := newBlock.Serialize()
		if err != nil {
			return err
		}

		//将区块存入桶中
		bk.Put(newBlock.Hash,serialize)

		//打开桶2
		bk2 := tx.Bucket([]byte(BUCKET_STATUS))
		if bk2 == nil {
			return errors.New("桶中没有区块")
		}

		//更新桶2
		bk2.Put([]byte(LAST_HASH),newBlock.Hash)
		//更新结构体
		bc.LastHash = newBlock.Hash

		return nil
	})

	return err
}

//创建一个迭代器对象,迭代器只能在有区块链的情况下才可以使用迭代器
func (bc *BlockChain)Iterator() *ChainIterator {
	iterator := ChainIterator{
		DB: bc.DB,
		CurrentHash: bc.LastHash,
	}

	return &iterator
}

//获取所有区块
func (bc *BlockChain) GetAllBlock()([]*Block,error) {
	var blockChain []*Block

	//使用迭代器获取
	iterator := bc.Iterator()
	for{
		if iterator.HasNext() {
			block, err := iterator.Next()
			if err != nil {
				return nil,err
			}
			blockChain = append(blockChain, block)
		}else {
			break
		}
	}
	return blockChain,nil
}

//定义一个方法，用来找某个地址的所有收入	zhang
func (bc *BlockChain) FindAllOutput(address string)[]transaction.UTXO {
	//先找所有的区块，再获取每一个区块中的所有的交易，再找是zhang的收入
	blocks, err := bc.GetAllBlock()
	if err != nil {
		fmt.Println("寻找失败",err.Error())
		return nil
	}
	//map结构：make[key]value
	//key：收入（input）所在的交易hash
	//value：[]int 表示output的位置的下标	1,2
	//用来储存某个人所有的收入的
	allOutputs := make([]transaction.UTXO,0)
	//获取每一个区块
	for _,block := range blocks{
		//获取每一个区块中的每一个交易
		for _, tx := range block.Txs {
			//找每一个交易中的所有的交易输出
			for outIndex, output := range tx.Output {
				if output.IsUnlock(address) {
					utxo := transaction.NewUTXO(tx.Txid,outIndex,output)
					allOutputs = append(allOutputs,utxo)
				}
			}
		}
	}
	return allOutputs
}

//寻找某个人的所有的消费（input）
func (bc *BlockChain) FindAllInput(address string) []transaction.Input {
	//先找到所有的区块，再获取到每一个区块中的所有的交易，再找是zhang的消费的
	allInputs := make([]transaction.Input,0)
	blocks, err := bc.GetAllBlock()
	if err != nil {
		fmt.Println("寻找失败：",err.Error())
		return nil
	}
	//找每一个区块
	for _, block := range blocks {
		//每一个区块中的每一个交易
		for _, tx := range block.Txs {
			//找每一个交易中的imput
			for _, input := range tx.Input {
				if input.Islocked(address) {
					allInputs = append(allInputs,input)
				}
			}
		}
	}
	return allInputs
}

/**
接收某个人的所有的收入和消费，并从所有的收入中去掉消费，剩下的就是可用的交易输出
 */
func (bc *BlockChain) FindSpendOutputs(alloutputs []transaction.UTXO,allinputs []transaction.Input,amount uint)([]transaction.UTXO,uint) {
	//获取每一个收入
	for _, input := range allinputs {
		for index, utxo := range alloutputs {
			//两个txid和索引下标都相等，表示这个utxo被用掉了
			//要从utxo中去掉这一笔收入，到最后剩下的utxo就是可用的余额
			if bytes.Compare(utxo.Txid, input.TXid) == 0 || utxo.Index == input.Vout {
				//如果遍历到最后一个，就不删除，防止下标越界
				if index >= len(alloutputs) {
					alloutputs = append(alloutputs[:index])
				}else {
					alloutputs = append(alloutputs[:index],alloutputs[index+1:]...)
				}
				break
			}
		}
	}

	//from这次要给to这个人实际的金额
	var totalAmount uint
	outputs := make([]transaction.UTXO,0)
	//遍历找到所有的余额
	for _, output := range alloutputs {
		totalAmount += output.Value
		outputs = append(outputs,output)
		if totalAmount >= amount {
			//钱够用了
			break
		}
	}

	return outputs,totalAmount
}

//创建一个Coinbase交易
func (bc *BlockChain)NewCoinbase(address string)(*transaction.Transaction,error) {
	if address == "" || len(address) == 0 {
		return nil,errors.New("地址不合法")
	}
	newCoinbase, err := transaction.NewCoinbase(address)
	if err != nil {
		return nil, err
	}
	return newCoinbase,nil
}

func (bc *BlockChain) NewTransaction(from,to string,amount uint)(*transaction.Transaction,error) {
	//1.构建input（交易输入）
	//a.在所有的交易中，去寻找可以使用的交易输出
	//所有可用的交易输出（余额） = 所有的收入 - 所有的消费
	spendInput := bc.FindAllInput(from)
	spendOutput := bc.FindAllOutput(from)
	//获取了from这个人的所有有用的交易输出（UTXO）
	//因为只需要找到满足本次交易的钱就可以了，不需要找所有的钱
	spendableOutputs, totalAmount := bc.FindSpendOutputs(spendOutput, spendInput,amount)
	//b.从所有的可用的交易输出中去，取出一部分，判断是否够用
	if spendableOutputs == nil {
		return nil,errors.New("没有可用的余额")
	}
	if totalAmount < amount {
		return nil,errors.New("余额不足，无法进行交易")
	}
	newTransaction, err := transaction.NewTransaction(from, to, amount, spendableOutputs)
	if err != nil {
		return nil,err
	}
	return newTransaction,nil
}

//获取到指定用户所有的余额
func (bc *BlockChain) ShowUserAmout(address string) uint {
	spendInput := bc.FindAllInput(address)
	spendOutput := bc.FindAllOutput(address)
	//该方法对金额进行了判断，如果大于传入的金额就不会再寻找了，所以在此我们传入一个uint最大值，使得其可以一直找到左右的余额
	_, amout := bc.FindSpendOutputs(spendOutput, spendInput, 18446744073709551615)
	return amout
}